// Copyright (c) 2023.  NetFoundry Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#include <stdint.h>
#include <stdlib.h>
#include "utils.h"

#if _WIN32
#include <crtdefs.h>
#else
#include <sys/param.h>
#endif
#include <tlsuv/queue.h>
#include <stdbool.h>
#include <stdarg.h>

#include "buffer.h"


/** incoming data chunk */
typedef struct chunk_s {
    uint8_t *buf;
    size_t len;

    STAILQ_ENTRY(chunk_s) next;
} chunk_t;

struct buffer_s {
    STAILQ_HEAD(incoming, chunk_s) chunks;
    size_t head_offset;
    size_t available;
};


buffer *new_buffer() {
    buffer *b = malloc(sizeof(buffer));
    b->head_offset = 0;
    b->available = 0;
    STAILQ_INIT(&b->chunks);

    return b;
}

void free_buffer(buffer* b) {
    if (b == NULL) return;
    while (!STAILQ_EMPTY(&b->chunks)) {
        chunk_t *chunk = STAILQ_FIRST(&b->chunks);
        STAILQ_REMOVE_HEAD(&b->chunks, next);
        free(chunk->buf);
        free(chunk);
    }
    free(b);
}

void buffer_cleanup(buffer *b) {

    if (STAILQ_EMPTY(&b->chunks)) {
        return;
    }

    chunk_t *chunk = STAILQ_FIRST(&b->chunks);
    if (chunk->len == b->head_offset) {
        STAILQ_REMOVE_HEAD(&b->chunks, next);
        b->head_offset = 0;
        free(chunk->buf);
        free(chunk);
    }
}

void buffer_push_back(buffer* b, size_t count) {
    b->available += count;
    b->head_offset -= count;
}

ssize_t buffer_get_next(buffer* b, size_t want, uint8_t** ptr) {
    if (STAILQ_EMPTY(&b->chunks)) {
        return -1;
    }

    chunk_t *chunk = STAILQ_FIRST(&b->chunks);
    if (chunk->len == b->head_offset) {
        STAILQ_REMOVE_HEAD(&b->chunks, next);
        b->head_offset = 0;
        free(chunk->buf);
        free(chunk);

        if (STAILQ_EMPTY(&b->chunks)) {
            return -1;
        }

        chunk = STAILQ_FIRST(&b->chunks);
    }
    int len = MIN(chunk->len - b->head_offset, want);
    *ptr = chunk->buf + b->head_offset;
    b->head_offset += len;
    b->available -= len;

    return len;
}

void buffer_append_copy(buffer *b, const uint8_t *buf, size_t len) {
    uint8_t *copy = calloc(len, sizeof(uint8_t));
    memcpy(copy, buf, len);
    buffer_append(b, copy, len);
}

void buffer_append(buffer* b, uint8_t *buf, size_t len) {
    chunk_t *e = malloc(sizeof(chunk_t));
    e->buf = buf;
    e->len = len;
    b->available += len;

    STAILQ_INSERT_TAIL(&b->chunks, e, next);
}

size_t buffer_available(buffer *b) {
    return b ? b->available : 0;
}

#define WRITE_BUF_CHUNK_SIZE 1024

void string_buf_init(string_buf_t *wb) {
    wb->fixed = false;
    wb->chunk_size = WRITE_BUF_CHUNK_SIZE;
    wb->chunk = malloc(wb->chunk_size);
    wb->buf = new_buffer();
    wb->wp = wb->chunk;
}

void string_buf_init_fixed(string_buf_t *wb, char *outbuf, size_t max) {
    wb->fixed = true;
    wb->chunk = (uint8_t *) outbuf;
    wb->wp = wb->chunk;
    wb->chunk_size = max;
    wb->buf = NULL;
}

size_t string_buf_size(string_buf_t *wb) {
    return buffer_available(wb->buf) + (wb->wp - wb->chunk);
}

int string_buf_append_byte(string_buf_t *wb, char c) {
    if (wb->wp - wb->chunk >= wb->chunk_size) {

        if (wb->fixed) { return -1; }

        buffer_append(wb->buf, wb->chunk, wb->wp - wb->chunk);
        wb->chunk = malloc(wb->chunk_size);
        wb->wp = wb->chunk;
    }
    *wb->wp++ = c;
    return 0;
}

int string_buf_appendn(string_buf_t *wb, const char *str, size_t len) {
    const char *s = str;

    size_t chunk_len;
    size_t copy_len;
    copy:
    chunk_len = wb->chunk + wb->chunk_size - wb->wp;
    copy_len = MIN(chunk_len, len);
    memcpy(wb->wp, s, copy_len);
    len -= copy_len;
    wb->wp += copy_len;
    s += copy_len;

    if (len > 0) {
        if (wb->fixed) { return -1; }

        buffer_append(wb->buf, wb->chunk, wb->wp - wb->chunk);
        wb->chunk = malloc(wb->chunk_size);
        wb->wp = wb->chunk;
        goto copy;
    }

    return 0;
}

int string_buf_append_urlsafe(string_buf_t *wb, const char *str) {
    static const char unsafe[] = " /:\"<>%{}|\\^`";

    if (str == NULL) {
        return 0;
    }

    const char *p = str;
    int rc = 0;
    while(*p && rc == 0) {
        char c = *p++;
        if (strchr(unsafe, c) == NULL) {
            rc = string_buf_append_byte(wb, c);
            continue;
        }

        char buf[4];
        snprintf(buf, sizeof(buf), "%%%02X", (int)c);
        rc = string_buf_appendn(wb, buf, 3);
    }
    return rc;
}

int string_buf_append(string_buf_t *wb, const char *str) {
    const char *s = str;

    copy:
    while (*s != '\0' && wb->wp < wb->chunk + wb->chunk_size) { *wb->wp++ = *s++; }

    if (*s != 0) {
        if (wb->fixed) { return -1; }

        buffer_append(wb->buf, wb->chunk, wb->wp - wb->chunk);
        wb->chunk = malloc(wb->chunk_size);
        wb->wp = wb->chunk;
        goto copy;
    }

    return 0;
}

char *string_buf_to_string(string_buf_t *wb, size_t *outlen) {
    size_t bytes_in_buffer = buffer_available(wb->buf);
    char *result = malloc(bytes_in_buffer + (wb->wp - wb->chunk) + 1);

    size_t copied = 0;
    while (copied < bytes_in_buffer) {
        uint8_t *copyp;
        size_t copy_len = buffer_get_next(wb->buf, bytes_in_buffer, &copyp);
        memcpy(result + copied, copyp, copy_len);
        copied += copy_len;
    }

    memcpy(result + copied, wb->chunk, wb->wp - wb->chunk);
    result[copied + (wb->wp - wb->chunk)] = 0;
    if (outlen) {
        *outlen = copied + (wb->wp - wb->chunk);
    }
    // after copy buffer contents is empty -- reset current chunk
    wb->wp = wb->chunk;
    
    return result;
}

void string_buf_free(string_buf_t *wb) {
    wb->wp = NULL;
    if (!wb->fixed) FREE(wb->chunk);
    wb->chunk = NULL;
    free_buffer(wb->buf);
    wb->buf = NULL;
}

string_buf_t *new_string_buf() {
    NEWP(wb, string_buf_t);
    string_buf_init(wb);
    return wb;
}

string_buf_t *new_fixed_string_buf(char *outbuf, size_t max) {
    NEWP(wb, string_buf_t);
    string_buf_init_fixed(wb, outbuf, max);
    return wb;
}

void delete_string_buf(string_buf_t *wb) {
    string_buf_free(wb);
    free(wb);
}

int string_buf_fmt(string_buf_t *wb, FORMAT_STRING(const char *fmt), ...) {
    va_list argp;
    va_start(argp, fmt);

    size_t avail_in_chunk = wb->chunk + wb->chunk_size - wb->wp;
    int len = vsnprintf((char *) wb->wp, avail_in_chunk, fmt, argp);
    va_end(argp);

    // fit into current chunk -- nothing else to do
    if (len < avail_in_chunk) {
        wb->wp += len;
        return len;
    }

    // can't allocate any more memory
    if (wb->fixed) return -1;

    // current chunk is not empty push into buffer
    if (wb->chunk != wb->wp) {
        buffer_append(wb->buf, wb->chunk, wb->wp - wb->chunk);
        wb->chunk = malloc(wb->chunk_size);
        wb->wp = wb->chunk;
    }

    va_start(argp, fmt);

    if (len < wb->chunk_size) {
        len = vsnprintf((char*)wb->wp, wb->chunk_size, fmt, argp);
        wb->wp += len;
    } else {
        // formatted string won't fit into chunk_size -- add directly to the buffer
        char *s = malloc(len + 1);
        len = vsnprintf(s, len + 1, fmt, argp);
        buffer_append(wb->buf, (uint8_t *)s, len);
    }
    va_end(argp);
    return len;
}

